#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (C) 1994  Ling Li
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.


import gobject

import logging, os, os.path, string, sys, thread, time
from threading import Thread, Event, Timer
from Queue import Queue, Empty

from ftplib_client import *
#from lftp_client import *
from ftp_exception import *
from utils import *

CMD_QUIT = 0
CMD_OPEN = 1
CMD_CD = 2
CMD_LIST = 3
CMD_LISTD = 4
CMD_PWD = 5
CMD_PUT = 6
CMD_GET = 7
CMD_MKDIR = 8
CMD_CHOWN = 9
CMD_CHMOD = 10
CMD_DELETE = 11
CMD_MOVE = 12
CMD_ABOR = 13
CMD_SIMPLE = 14

STATUS_NONE = 0
STATUS_CONNECTING = 1
STATUS_IDLE = 2
STATUS_RUNNING = 3
STATUS_ABORT = 4

ERRNO_ABORT = -1
ERRNO_TIMEDOUT = -2

class FTPEngine(gobject.GObject, Thread):
    """
    This is a control object for FTP task. All the FTP request from remote
    view is sent to this object, this object will process them using real
    ftp program wrapper one by one.
    
    Every FTPEngine object has a is_idle Event object, when a request is
    processed, the object is clear, all the following command should wait
    until the previous command finished.

    FTPEngine use a real ftp program(such as lftp) wrapper to process FTP
    command, in all the cases it will wait until the FTP command return.
    So FTPEngine has a thread to process FTP commands.

    FTPEngine is also a sub-class of GObject class. When process of FTP
    command finished, signals are emitted via gobject_idle.
    """

    __gsignals__ = {
        'interrupted' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT,)),
        'idle' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT,)),
        'delay' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT,)),
        'log-message':
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
         (gobject.TYPE_INT, gobject.TYPE_STRING)),
        'quit' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'idle-connection-closed' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'open-succeeded' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'open-failed' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
         (gobject.TYPE_INT, gobject.TYPE_STRING)),
        'welcome-message' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
         (gobject.TYPE_STRING,)),
        'list-succeeded' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'list-failed' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
         (gobject.TYPE_INT, gobject.TYPE_STRING)),
        'listd-succeeded' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'listd-failed' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
         (gobject.TYPE_INT, gobject.TYPE_STRING)),
        'pwd-succeeded' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'pwd-failed' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
         (gobject.TYPE_INT, gobject.TYPE_STRING)),
        'cd-succeeded' :
       (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'cd-failed' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
         (gobject.TYPE_INT, gobject.TYPE_STRING)),
        'move-succeeded' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'move-failed' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
         (gobject.TYPE_INT, gobject.TYPE_STRING)),
        'delete-succeeded' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'delete-failed' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
         (gobject.TYPE_INT, gobject.TYPE_STRING)),
        'mkdir-succeeded' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'mkdir-failed' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
         (gobject.TYPE_INT, gobject.TYPE_STRING)),
        'put-succeeded' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'put-failed' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
         (gobject.TYPE_INT, gobject.TYPE_STRING)),
        'get-succeeded' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'get-failed' :
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
         (gobject.TYPE_INT, gobject.TYPE_STRING)),
        }

    # the status of the ftp engine
    status = None
    
    # total_idle is idle time since last useful command
    total_idle = 0
    
    # Timer object for idle
    idle_timer = None
    
    # Timer object for delay
    delay_timer = None

    # If true, start/stop idle will change idle time and start/stop send of
    # idle event, otherwise, only __is_idle event will be changed.
    __change_idle = True

    # __queue_lock is used to make command_queue be visited on in one thread.
    __queue_lock = None

    # __command_queue is a place which is used to store ftp commands need
    # processed. FTPEngine object will add command to the queue and FTPClient
    # object fetch command from the queue.
    # __command_queue can be visited in only one thread, and the access to
    # it is restricted by __queue_lock
    __command_queue = None

    __welcome_message = None

    def __init__(self, rview):
        self.__gobject_init__()
        Thread.__init__(self)

        self.__remote_view = rview
        self.__coralftp = rview.main_window.coralftp
        self.__is_idle = Event()
        self.__queue_lock = Event()
        self.__quit_event = Event()

        self.__ftp_client = FTPLibClient(self)
        self.__command_queue = Queue(8)

        self.__read_config()
        self.status = STATUS_NONE

    def __read_config(self):
        config = self.config
        self.connect_retry_delay = config_value(
            config, 'general', 'connect_retry_delay')
        self.connect_retry_count = config_value(
            config, 'general', 'connect_retry_count')
        self.send_keep_alive_command = config_value(
            config, 'general', 'send_keep_alive_command')
        self.keep_alive_command = config_value(
            config, 'general', 'keep_alive_command')
        self.send_keep_alive_interval = config_value(
            config, 'general', 'send_keep_alive_interval')
        self.stop_send_keep_alive = config_value(
            config, 'general', 'stop_send_keep_alive')
        self.stop_keep_alive_after = 60 * config_value(
            config, 'general', 'stop_keep_alive_after')
        
    def run(self):
        self.__pid = thread.get_ident()
        logging.debug('ftp thread start, pid is %d' % self.__pid)
        self.__coralftp.register_thread('ftp')
        self.__ftp_client.run()
        self.__queue_lock.set()
        self.__is_idle.set()

        while not self.quit_event.isSet():
            try:
                func = args = change_idle = None
                self.__queue_lock.wait()
                func, args, change_idle = self.__command_queue.get(True, 1)
                logging.debug("ftp thread get new command %s, %s, %s" \
                              % (func, args, change_idle))
                if self.__is_idle.isSet():
                    self.stop_idle(change_idle)
                self.__result = func(*args)
                if self.__command_queue.empty():
                    self.start_idle(change_idle)
            except Empty, e:
                pass
            
        logging.debug('before emit quit')
        gobject.idle_add(self.emit, 'quit')
        logging.debug('ftp thread quit')
        return

    def __execute_in_thread(self, func, *args):
        """This function make put the method into command queue, and make it
        run in ftp thread."""
        if not self.__queue_lock.isSet():
            raise RuntimeError
        self.__command_queue.put((func, args, self.__change_idle), True)

    def __getattr__(self, name):
        if name == 'remote_view':
            return self.__remote_view
        elif name == 'is_idle_event':
            return self.__is_idle
        elif name == 'is_idle':
            return self.status == STATUS_IDLE
        elif name == 'quit_event':
            return self.__quit_event
        elif name == 'config':
            return self.__remote_view.config
        elif name == 'log_updater':
            return self.__remote_view.main_window.log_view.updater
        elif name == 'site_info':
            return self.__site_info
        elif name == 'welcome_message':
            return self.__welcome_message
        elif name == 'coralftp':
            return self.__coralftp
        else:
            raise AttributeError, name

    def do_idle_connection_closed(self):
        self.status = STATUS_NONE
        gobject.idle_add(self.quit)

    def abort(self):
        logging.debug("ftp_engine.abort runs in %s" \
                      % self.__coralftp.get_thread_name())
        if self.delay_timer:
            self.delay_timer.cancel()
            self.delay_timer = None
            self.delayed = None
        self.__queue_lock.clear()
        while not self.__command_queue.empty():
            self.__command_queue.get_nowait()
        if self.status == STATUS_RUNNING:
            self.status = STATUS_ABORT
            try:
                self.__ftp_client.interrupt()
            except EOFError:
                self.__queue_lock.set()
                self.quit()
        self.__queue_lock.set() 

    def quit(self):
        logging.debug('quit command execute in %s' % \
                      self.__coralftp.get_thread_name())
        if not self.__is_idle.isSet():
            self.abort()
            self.__is_idle.wait()
        self.__quit_event.set()
        self.__change_idle = False

        # send ftp quit commnad
        if self.status > STATUS_CONNECTING:
            try:
                self.__ftp_client.quit()
            except FTPTempException, (errno, errmsg):
                if errno == 421:
                    pass
                else:
                    raise ValueError
        self.status = STATUS_NONE
        self.stop_idle(False)
        logging.debug('quit command finished')
        return

    def start_idle(self, change_idle, finished_event = None):
        def idle():
            if self.status == STATUS_IDLE:
                self.total_idle = self.total_idle + 1
                gobject.idle_add(self.emit, 'idle', self.total_idle)
                self.idle_timer = Timer(1, idle)
                self.idle_timer.start()
            return

        self.__is_idle.set()
        if self.status > STATUS_CONNECTING:
            self.__ftp_client.start_monitor()
        
        if change_idle and self.status > STATUS_CONNECTING:
            self.status = STATUS_IDLE
            self.total_idle = 0
            logging.debug('start idle')
            self.idle_timer = Timer(1, idle)
            self.idle_timer.start()

        if finished_event != None:
            finished_event.set()
        logging.debug('start idle finished')
        return
            
    def stop_idle(self, change_idle, finished_event = None):
        logging.debug('stop_idle runs')
        self.__is_idle.clear()
        if self.status > STATUS_CONNECTING:
            logging.debug('before stop monitor')
            self.__ftp_client.stop_monitor()
            logging.debug('after stop monitor')
        
        if change_idle or self.status <= STATUS_CONNECTING:
            logging.debug('before gobject.idle_add emit')
            gobject.idle_add(self.emit, 'idle', -1)
            logging.debug('after gobject.idle_add emit')
            if self.idle_timer:
                self.idle_timer.cancel()
                self.idle_timer = None
        
        if change_idle:
            self.status = STATUS_RUNNING
        
        if finished_event != None:
            finished_event.set()
        logging.debug('stop_idle finished')
        return
            
    def do_idle(self, idle_time):
        def send_noop():
            if self.keep_alive_command == 'NOOP':
                cmd = self.__ftp_client.new_command(CMD_SIMPLE, 'NOOP')
                try:
                    self.__ftp_client.execute_command(cmd)
                except FTPException, (errno, errmsg):
                    if errno == 421:
                        gobject.idle_add(self.emit, 'idle-connection-closed')
            elif self.keep_alive_command == 'PWD':
                self.pwd()
                
        if self.send_keep_alive_command \
               and (not self.stop_send_keep_alive \
                    or self.total_idle <= self.stop_keep_alive_after) \
                    and idle_time % self.send_keep_alive_interval == 0:
            self.__change_idle = False
            self.__execute_in_thread(send_noop)
            self.__change_idle = True
        return

    def do_welcome_message(self, message):
        self.__welcome_message = message
        return
            
    def open(self, site_info):
        def on_delay():
            """This function is used to show delay message on status bar,
            and when delay time reached, send run command again."""
            # If user press disconnect button, stop idle counting
            if self.status != STATUS_CONNECTING:
                return False
            if self.delayed != None:
                self.delayed = self.delayed - 1
                gobject.idle_add(self.emit, 'delay', self.delayed)
                if self.delayed == 0:
                    # run connect again
                    self.retried = self.retried + 1
                    self.delayed = None
                    self.delay_timer = None
                    self.__change_idle = False
                    self.__execute_in_thread(self.open, site_info)
                    self.__change_idle = True
                    return False
                else:
                    return True
            return
        
        logging.debug('open command executed in %s' \
                      % self.__coralftp.get_thread_name())
        if thread.get_ident() != self.__pid:
            self.retried = 1
            self.__change_idle = False
            self.__execute_in_thread(self.open, site_info)
            self.__change_idle = True
            return

        self.__site_info = site_info
        # change status to STATUS_CONNECTING
        self.status = STATUS_CONNECTING
        cmd = self.__ftp_client.new_command(CMD_OPEN, site_info)
        try:
            self.__ftp_client.execute_command(cmd)
            self.status = STATUS_IDLE
            gobject.idle_add(self.emit, 'open-succeeded')
        except FTPException, (errno, errmsg):
            gobject.idle_add(self.emit, 'log-message', 0, errmsg + '\r\n')
            if self.retried >= self.connect_retry_count:
                gobject.idle_add(self.emit, 'log-message', 0, 'Failed to connect')
                gobject.idle_add(self.emit, 'open-failed', 0, '')
                # change status to NONE
                self.status = STATUS_NONE
            else:
                self.delayed = self.connect_retry_delay
                self.delay_timer = Timer(1, on_delay)
                self.delay_timer.start()
        except NetworkException, (errno, errmsg):
            gobject.idle_add(self.emit, 'log-message', 0, errmsg + '\r\n')
            if self.retried >= self.connect_retry_count:
                gobject.idle_add(self.emit, 'log-message', 0, 'Failed to connect')
                gobject.idle_add(self.emit, 'open-failed', 0, '')
                # change status to NONE
            else:
                self.delayed = self.connect_retry_delay
                self.delay_timer = Timer(1, on_delay)
                self.delay_timer.start()
        logging.debug('open command finished')

    def pwd(self, updater=None):
        logging.debug('pwd command run in %s' \
                      % self.__coralftp.get_thread_name())
        if thread.get_ident() != self.__pid:
            self.__execute_in_thread(self.pwd, updater)
            return

        cmd = self.__ftp_client.new_command(CMD_PWD, updater)
        try:
            self.__ftp_client.execute_command(cmd)
            gobject.idle_add(self.emit, 'pwd-succeeded')
        except FTPException, (errno, errmsg):
            gobject.idle_add(self.emit, 'pwd-failed', errno, errmsg)
        except NetworkException, (errno, errmsg):
            gobject.idle_add(self.emit, 'log-message', 0, "%d %s" % (errno, errmsg))
            gobject.idle_add(self.emit, 'pwd-failed', errno, errmsg)
        logging.debug('pwd command finished')

    def list(self, updater=None, dir='', clear_cache=False):
        logging.debug('list command execute in %s' \
                      % self.__coralftp.get_thread_name())
        if thread.get_ident() != self.__pid:
            self.__execute_in_thread(self.list, updater, dir, clear_cache)
            return

        cmd = self.__ftp_client.new_command(CMD_LIST,
                                            updater, dir, clear_cache)
        try:
            self.__ftp_client.execute_command(cmd)
            logging.debug('list-succeeded')
            gobject.idle_add(self.emit, 'list-succeeded')
        except FTPException, (errno, errmsg):
            logging.debug('list-failed')
            gobject.idle_add(self.emit, 'list-failed', errno, errmsg)
        except NetworkException, error:
            if isinstance(error, list):
                errno, errmsg = error
            else:
                errno = 0
                errmsg = error.__str__() + '\n'
            gobject.idle_add(self.emit, 'log-message', 0, "%d %s" % (errno, errmsg))
            gobject.idle_add(self.emit, 'list-failed', errno, errmsg)
        logging.debug('list command finished')

    def listd(self, apath):
        if thread.get_ident() != self.__pid:
            self.__execute_in_thread(self.listd, apath)
            return

        cmd = self.__ftp_client.new_command(CMD_LISTD, apath)
        try:
            if self.__ftp_client.execute_command(cmd):
                logging.debug('listd-succeeded')
                gobject.idle_add(self.emit, 'listd-succeeded')
            else:
                logging.debug('listd-failed')
                gobject.idle_add(self.emit, 'listd-failed', 0, '')
        except FTPException, (errno, errmsg):
            logging.debug('listd-failed')
            gobject.idle_add(self.emit, 'list-failed', errno, errmsg)
        except NetworkException, (errno, errmsg):
            gobject.idle_add(self.emit, 'log-message', 0, "%d %s" % (errno, errmsg))
            gobject.idle_add(self.emit, 'listd-failed', errno, errmsg)

    def cd(self, new_path):
        logging.debug('cd command execute in %s'\
                      % self.__coralftp.get_thread_name())
        if thread.get_ident() != self.__pid:
            self.__execute_in_thread(self.cd, new_path)
            return

        cmd = self.__ftp_client.new_command(CMD_CD, new_path)
        try:
            self.__ftp_client.execute_command(cmd)
            logging.debug('cd-succeeded')
            gobject.idle_add(self.emit, 'cd-succeeded')
        except FTPException, (errno, errmsg):
            logging.debug('cd-failed')
            gobject.idle_add(self.emit, 'cd-failed', errno, errmsg)
        except NetworkException, (errno, errmsg):
            gobject.idle_add(self.emit, 'log-message', 0, "%d %s" % (errno, errmsg))
            gobject.idle_add(self.emit, 'cd-failed', errno, errmsg)
        logging.debug('cd command finished')

    def put(self, source, target, mode='b', rupdater=None, fupdater=None):
        if thread.get_ident() != self.__pid:
            self.__execute_in_thread(self.put, source, target, mode,
                                     rupdater)
            return

        cmd = self.__ftp_client.new_command(CMD_PUT, source, target, mode,
                                            rupdater)
        try:
            self.__ftp_client.execute_command(cmd)
            if self.status == STATUS_ABORT:
                gobject.idle_add(self.emit, 'put-failed', ERRNO_ABORT, 'Aborted by user')
                logging.debug('put-interrupted')
            else:
                gobject.idle_add(self.emit, 'put-succeeded')
                logging.debug('put-succeeded %s --> %s' % (source, target))
        except FTPException, (errno, errmsg):
            logging.debug('put-failed %d %s' % (errno, errmsg))
            gobject.idle_add(self.emit, 'put-failed', errno, errmsg)
        except NetworkException, (errno, errmsg):
            gobject.idle_add(self.emit, 'log-message', 0, "%d %s" % (errno, errmsg))
            gobject.idle_add(self.emit, 'put-failed', errno, errmsg)

    def get(self, source, target, mode='b', resume=0, rupdater=None, fupdater=None):
        if thread.get_ident() != self.__pid:
            self.__execute_in_thread(self.get, source, target, mode, resume,
                                     rupdater, fupdater)
            return

        cmd = self.__ftp_client.new_command(CMD_GET, source, target, mode,
                                            resume, rupdater, fupdater)
        try:
            self.__ftp_client.execute_command(cmd)
            if self.status == STATUS_ABORT:
                gobject.idle_add(self.emit, 'get-failed', ERRNO_ABORT, 'Aborted by user')
                logging.debug('get-interrupted')
            else:
                gobject.idle_add(self.emit, 'get-succeeded')
                logging.debug('get-succeeded %s --> %s' % (source, target))
        except FTPException, (errno, errmsg):
            logging.debug('get-failed %d %s' % (errno, errmsg))
            gobject.idle_add(self.emit, 'get-failed', errno, errmsg)
        except NetworkException, error:
            logging.debug("NetworkException: %s", error)
            if isinstance(error, list):
                errno, errmsg = error
            else:
                errno = 0
                errmsg = error + '\n'
            gobject.idle_add(self.emit, 'log-message', 0, "%d %s" % (errno, errmsg))
            gobject.idle_add(self.emit, 'get-failed', errno, errmsg)
        except IOError, ex:
            gobject.idle_add(self.emit, 'log-message', 0, ex.__str__())
            gobject.idle_add(self.emit, 'get-failed', 0, ex.__str__())
            
    def delete(self, name, isdir):
        if thread.get_ident() != self.__pid:
            self.__execute_in_thread(self.delete, name, isdir)
            return

        cmd = self.__ftp_client.new_command(CMD_DELETE, name, isdir)
        try:
            self.__ftp_client.execute_command(cmd)
            logging.debug('delete-succeeded %s' % name)
            gobject.idle_add(self.emit, 'delete-succeeded')
        except FTPException, (errno, errmsg):
            logging.debug('delete-failed: %d %s' % (errno, errmsg))
            gobject.idle_add(self.emit, 'delete-failed', errno, errmsg)
        except NetworkException, (errno, errmsg):
            gobject.idle_add(self.emit, 'log-message', 0, "%d %s" % (errno, errmsg))
            gobject.idle_add(self.emit, 'delete-failed', errno, errmsg)

    def move(self, old_name, new_name):
        if thread.get_ident() != self.__pid:
            self.__execute_in_thread(self.move, old_name, new_name)
            return

        cmd = self.__ftp_client.new_command(CMD_MOVE, old_name, new_name)
        try:
            self.__ftp_client.execute_command(cmd)
            gobject.idle_add(self.emit, 'move-succeeded')
        except FTPException, (errno, errmsg):
            gobject.idle_add(self.emit, 'move-failed', old_name, new_name, errno, errmsg)
        except NetworkException, (errno, errmsg):
            gobject.idle_add(self.emit, 'log-message', 0, "%d %s" % (errno, errmsg))
            gobject.idle_add(self.emit, 'move-failed', errno, errmsg)

    def mkdir(self, dirname):
        if thread.get_ident() != self.__pid:
            self.__execute_in_thread(self.mkdir, dirname)
            return

        cmd = self.__ftp_client.new_command(CMD_MKDIR, dirname)
        try:
            self.__ftp_client.execute_command(cmd)
            logging.debug('mkdir-succeeded')
            gobject.idle_add(self.emit, 'mkdir-succeeded')
        except FTPException, (errno, errmsg):
            logging.debug('mkdir-failed')
            gobject.idle_add(self.emit, 'mkdir-failed', dirname, errno, errmsg)
        except NetworkException, (errno, errmsg):
            gobject.idle_add(self.emit, 'log-message', 0, "%d %s" % (errno, errmsg))
            gobject.idle_add(self.emit, 'mkdir-failed', errno, errmsg)

gobject.type_register(FTPEngine)

class FTPClient:
    """
    FTPClient is a interface for FTP client programs used by CoralFTP. All the
    FTP command is running in FTPThread thread.
    """

    def __init__(self):
        pass

    def __del__(self):
        pass

    def new_command(self, command_type, *args):
        return None

    def execute_command(self, command, *args):
        return command.execute(*args)

    def interrupt(self):
        return None

    def quit(self):
        return None

